/*
 * @(#)Response.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001  Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.SequenceInputStream;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Date;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class represents an intermediate response. It's used internally by the
 * modules. When all modules have handled the response then the HTTPResponse
 * fills in its fields with the data from this class.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 */
public final class Response implements RoResponse, GlobalConstants, Cloneable {
    /** This contains a list of headers which may only have a single value */
    private static final Hashtable singleValueHeaders;

    /** our http connection */
    private HTTPConnection connection;

    /** our stream demux */
    private StreamDemultiplexor stream_handler;

    /** the HTTPResponse we're coupled with */
    HTTPResponse http_resp;

    /** the timeout for read operations */
    int timeout = 0;

    /**
     * our input stream (usually from the stream demux). Push input streams onto
     * this if necessary.
     */
    public InputStream inp_stream;

    /** our response input stream from the stream demux */
    private RespInputStream resp_inp_stream = null;

    /** the method used in the request */
    private String method;

    /** the resource in the request (for debugging purposes) */
    String resource;

    /** was a proxy used for the request? */
    private boolean used_proxy;

    /** did the request contain an entity? */
    private boolean sent_entity;

    /** the status code returned. */
    int StatusCode = 0;

    /** the reason line associated with the status code. */
    String ReasonLine;

    /** the HTTP version of the response. */
    String Version;

    /** the final URI of the document. */
    URI EffectiveURI = null;

    /** any headers which were received and do not fit in the above list. */
    CIHashtable Headers = new CIHashtable();

    /** any trailers which were received and do not fit in the above list. */
    CIHashtable Trailers = new CIHashtable();

    /**
     * the message length of the response if either there is no data (in which
     * case ContentLength=0) or if the message length is controlled by a
     * Content-Length header. If neither of these, then it's -1
     */
    int ContentLength = -1;

    /** this indicates how the length of the entity body is determined */
    int cd_type = CD_HDRS;

    /** the data (body) returned. */
    byte[] Data = null;

    /** signals if we in the process of reading the headers */
    boolean reading_headers = false;

    /** signals if we have got and parsed the headers yet */
    boolean got_headers = false;

    /** signals if we have got and parsed the trailers yet */
    boolean got_trailers = false;

    /** remembers any exception received while reading/parsing headers */
    private IOException exception = null;

    /** should this response be handled further? */
    boolean final_resp = false;

    /** should the request be retried by the application? */
    boolean retry = false;

    private static final Logger log = Logger.getLogger(Response.class);

    static {
      /*
       * This static initializer creates a hashtable of header names that should
       * only have at most a single value in a server response. Other headers that
       * may have multiple values (ie Set-Cookie) will have their values combined
       * into one header, with individual values being separated by commas.
       */
        String[] singleValueHeaderNames =
                {"age", "location", "content-base", "content-length", "content-location", "content-md5", "content-range",
                 "content-type", "date", "etag", "expires", "proxy-authenticate", "retry-after",};

        singleValueHeaders = new Hashtable(singleValueHeaderNames.length);
        for (int idx = 0; idx < singleValueHeaderNames.length; idx++)
            singleValueHeaders.put(singleValueHeaderNames[idx], singleValueHeaderNames[idx]);
    }

    // Constructors

    /** Creates a new Response and registers it with the stream-demultiplexor. */
    Response(Request request, boolean used_proxy, StreamDemultiplexor stream_handler) throws IOException {
        this.connection = request.getConnection();
        this.method = request.getMethod();
        this.resource = request.getRequestURI();
        this.used_proxy = used_proxy;
        this.stream_handler = stream_handler;
        sent_entity = (request.getData() != null) ? true : false;

        stream_handler.register(this, request);
        resp_inp_stream = stream_handler.getStream(this);
        inp_stream = resp_inp_stream;
    }

    /**
     * Creates a new Response that reads from the given stream. This is used for
     * the CONNECT subrequest which is used in establishing an SSL tunnel through
     * a proxy.
     *
     * @param request
     *         the subrequest
     * @param is
     *         the input stream from which to read the headers and data.
     */
    Response(Request request, InputStream is) throws IOException {
        this.connection = request.getConnection();
        this.method = request.getMethod();
        this.resource = request.getRequestURI();
        used_proxy = false;
        stream_handler = null;
        sent_entity = (request.getData() != null) ? true : false;
        inp_stream = is;
    }

    /**
     * Create a new response with the given info. This is used when creating a
     * response in a requestHandler().
     * <p/>
     * If <var>data</var> is not null then that is used; else if the
     * <var>is</var> is not null that is used; else the entity is empty. If the
     * input stream is used then <var>cont_len</var> specifies the length of the
     * data that can be read from it, or -1 if unknown.
     *
     * @param version
     *         the response version (such as "HTTP/1.1")
     * @param status
     *         the status code
     * @param reason
     *         the reason line
     * @param headers
     *         the response headers
     * @param data
     *         the response entity
     * @param is
     *         the response entity as an InputStream
     * @param cont_len
     *         the length of the data in the InputStream
     */
    public Response(String version, int status, String reason, NVPair[] headers, byte[] data, InputStream is,
                    int cont_len) {
        this.Version = version;
        this.StatusCode = status;
        this.ReasonLine = reason;
        if (headers != null)
            for (int idx = 0; idx < headers.length; idx++)
                setHeader(headers[idx].getName(), headers[idx].getValue());
        if (data != null)
            this.Data = data;
        else if (is == null)
            this.Data = new byte[0];
        else {
            this.inp_stream = is;
            ContentLength = cont_len;
        }

        got_headers = true;
        got_trailers = true;
    }

    // Methods

    /**
     * give the status code for this request. These are grouped as follows:
     * <UL>
     * <LI>1xx - Informational (new in HTTP/1.1)
     * <LI>2xx - Success
     * <LI>3xx - Redirection
     * <LI>4xx - Client Error
     * <LI>5xx - Server Error
     * </UL>
     *
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public final int getStatusCode() throws IOException {
        if (!got_headers)
            getHeaders(true);
        return StatusCode;
    }

    /**
     * give the reason line associated with the status code.
     *
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public final String getReasonLine() throws IOException {
        if (!got_headers)
            getHeaders(true);
        return ReasonLine;
    }

    /**
     * get the HTTP version used for the response.
     *
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public final String getVersion() throws IOException {
        if (!got_headers)
            getHeaders(true);
        return Version;
    }

    /**
     * Wait for either a '100 Continue' or an error.
     *
     * @return the return status.
     */
    int getContinue() throws IOException {
        getHeaders(false);
        return StatusCode;
    }

    /**
     * get the final URI of the document. This is set if the original request was
     * deferred via the "moved" (301, 302, or 303) return status.
     *
     * @return the new URI, or null if not redirected
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public final URI getEffectiveURI() throws IOException {
        if (!got_headers)
            getHeaders(true);
        return EffectiveURI;
    }

    /** set the final URI of the document. This is only for internal use. */
    public void setEffectiveURI(URI final_uri) {
        EffectiveURI = final_uri;
    }

    /**
     * get the final URL of the document. This is set if the original request was
     * deferred via the "moved" (301, 302, or 303) return status.
     *
     * @throws IOException
     *         If any exception occurs on the socket.
     * @see #getEffectiveURI
     * @deprecated use getEffectiveURI() instead
     */
    public final URL getEffectiveURL() throws IOException {
        return getEffectiveURI().toURL();
    }

    /**
     * set the final URL of the document. This is only for internal use.
     *
     * @see #setEffectiveURI
     * @deprecated use setEffectiveURI() instead
     */
    public void setEffectiveURL(URL final_url) {
        try {
            setEffectiveURI(new URI(final_url));
        } catch (ParseException pe) {
            throw new Error(pe.toString());
        } // shouldn't happen
    }

    /**
     * retrieves the field for a given header.
     *
     * @param hdr
     *         the header name.
     * @return the value for the header, or null if non-existent.
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public String getHeader(String hdr) throws IOException {
        if (!got_headers)
            getHeaders(true);
        return (String)Headers.get(hdr.trim());
    }

    /**
     * retrieves the field for a given header. The value is parsed as an int.
     *
     * @param hdr
     *         the header name.
     * @return the value for the header if the header exists
     * @throws NumberFormatException
     *         if the header's value is not a number or
     *         if the header does not exist.
     * @throws IOException
     *         if any exception occurs on the socket.
     */
    public int getHeaderAsInt(String hdr) throws IOException, NumberFormatException {
        String val = getHeader(hdr);
        if (val == null)
            throw new NumberFormatException("null");
        return Integer.parseInt(val);
    }

    /**
     * retrieves the field for a given header. The value is parsed as a date; if
     * this fails it is parsed as a long representing the number of seconds since
     * 12:00 AM, Jan 1st, 1970. If this also fails an IllegalArgumentException is
     * thrown.
     * <p/>
     * Note: When sending dates use Util.httpDate().
     *
     * @param hdr
     *         the header name.
     * @return the value for the header, or null if non-existent.
     * @throws IOException
     *         If any exception occurs on the socket.
     * @throws IllegalArgumentException
     *         If the header cannot be parsed as a
     *         date or time.
     */
    public Date getHeaderAsDate(String hdr) throws IOException, IllegalArgumentException {
        String raw_date = getHeader(hdr);
        if (raw_date == null)
            return null;

        // asctime() format is missing an explicit GMT specifier
        if (raw_date.toUpperCase().indexOf("GMT") == -1 && raw_date.indexOf(' ') > 0)
            raw_date += " GMT";

        Date date;

        try {
            date = Util.parseHttpDate(raw_date);
        } catch (IllegalArgumentException iae) {
            long time;
            try {
                time = Long.parseLong(raw_date);
            } catch (NumberFormatException nfe) {
                throw iae;
            }
            if (time < 0)
                time = 0;
            date = new Date(time * 1000L);
        }

        return date;
    }

    /**
     * Set a header field in the list of headers. If the header already exists it
     * will be overwritten; otherwise the header will be added to the list. This
     * is used by some modules when they process the header so that higher level
     * stuff doesn't get confused when the headers and data don't match.
     *
     * @param header
     *         The name of header field to set.
     * @param value
     *         The value to set the field to.
     */
    public void setHeader(String header, String value) {
        Headers.put(header.trim(), value.trim());
    }

    /**
     * Removes a header field from the list of headers. This is used by some
     * modules when they process the header so that higher level stuff doesn't
     * get confused when the headers and data don't match.
     *
     * @param header
     *         The name of header field to remove.
     */
    public void deleteHeader(String header) {
        Headers.remove(header.trim());
    }

    /**
     * Retrieves the field for a given trailer. Note that this should not be
     * invoked until all the response data has been read. If invoked before, it
     * will force the data to be read via <code>getData()</code>.
     *
     * @param trailer
     *         the trailer name.
     * @return the value for the trailer, or null if non-existent.
     * @throws IOException
     *         If any exception occurs on the socket.
     */
    public String getTrailer(String trailer) throws IOException {
        if (!got_trailers)
            getTrailers();
        return (String)Trailers.get(trailer.trim());
    }

    /**
     * Retrieves the field for a given tailer. The value is parsed as an int.
     *
     * @param trailer
     *         the tailer name.
     * @return the value for the trailer if the trailer exists
     * @throws NumberFormatException
     *         if the trailer's value is not a number or
     *         if the trailer does not exist.
     * @throws IOException
     *         if any exception occurs on the socket.
     */
    public int getTrailerAsInt(String trailer) throws IOException, NumberFormatException {
        String val = getTrailer(trailer);
        if (val == null)
            throw new NumberFormatException("null");
        return Integer.parseInt(val);
    }

    /**
     * Retrieves the field for a given trailer. The value is parsed as a date; if
     * this fails it is parsed as a long representing the number of seconds since
     * 12:00 AM, Jan 1st, 1970. If this also fails an IllegalArgumentException is
     * thrown.
     * <p/>
     * Note: When sending dates use Util.httpDate().
     *
     * @param trailer
     *         the trailer name.
     * @return the value for the trailer, or null if non-existent.
     * @throws IllegalArgumentException
     *         if the trailer's value is neither a
     *         legal date nor a number.
     * @throws IOException
     *         if any exception occurs on the socket.
     * @throws IllegalArgumentException
     *         If the header cannot be parsed as a
     *         date or time.
     */
    public Date getTrailerAsDate(String trailer) throws IOException, IllegalArgumentException {
        String raw_date = getTrailer(trailer);
        if (raw_date == null)
            return null;

        // asctime() format is missing an explicit GMT specifier
        if (raw_date.toUpperCase().indexOf("GMT") == -1 && raw_date.indexOf(' ') > 0)
            raw_date += " GMT";

        Date date;

        try {
            date = Util.parseHttpDate(raw_date);
        } catch (IllegalArgumentException iae) {
            // some servers erroneously send a number, so let's try that
            long time;
            try {
                time = Long.parseLong(raw_date);
            } catch (NumberFormatException nfe) {
                throw iae;
            } // give up
            if (time < 0)
                time = 0;
            date = new Date(time * 1000L);
        }

        return date;
    }

    /**
     * Set a trailer field in the list of trailers. If the trailer already exists
     * it will be overwritten; otherwise the trailer will be added to the list.
     * This is used by some modules when they process the trailer so that higher
     * level stuff doesn't get confused when the trailer and data don't match.
     *
     * @param trailer
     *         The name of trailer field to set.
     * @param value
     *         The value to set the field to.
     */
    public void setTrailer(String trailer, String value) {
        Trailers.put(trailer.trim(), value.trim());
    }

    /**
     * Removes a trailer field from the list of trailers. This is used by some
     * modules when they process the trailer so that higher level stuff doesn't
     * get confused when the trailers and data don't match.
     *
     * @param trailer
     *         The name of trailer field to remove.
     */
    public void deleteTrailer(String trailer) {
        Trailers.remove(trailer.trim());
    }

    /**
     * Reads all the response data into a byte array. Note that this method won't
     * return until <em>all</em> the data has been received (so for instance
     * don't invoke this method if the server is doing a server push). If
     * getInputStream() had been previously called then this method only returns
     * any unread data remaining on the stream and then closes it.
     *
     * @return an array containing the data (body) returned. If no data was
     *         returned then it's set to a zero-length array.
     * @throws IOException
     *         If any io exception occured while reading the data
     * @see #getInputStream()
     */
    public synchronized byte[] getData() throws IOException {
        if (!got_headers)
            getHeaders(true);

        if (Data == null) {
            try {
                readResponseData(inp_stream);
            } catch (InterruptedIOException ie) // don't intercept
            {
                throw ie;
            } catch (IOException ioe) {
                log.error("Response:  (" + inp_stream.hashCode() + ")", ioe);

                try {
                    inp_stream.close();
                } catch (Exception e) {
                }
                throw ioe;
            }

            inp_stream.close();
        }

        return Data;
    }

    /**
     * Gets an input stream from which the returned data can be read. Note that
     * if getData() had been previously called it will actually return a
     * ByteArrayInputStream created from that data.
     *
     * @return the InputStream.
     * @throws IOException
     *         If any exception occurs on the socket.
     * @see #getData()
     */
    public synchronized InputStream getInputStream() throws IOException {
        if (!got_headers)
            getHeaders(true);

        if (Data == null)
            return inp_stream;
        else
            return new ByteArrayInputStream(Data);
    }

    /**
     * Some responses such as those from a HEAD or with certain status codes
     * don't have an entity. This is detected by the client and can be queried
     * here. Note that this won't try to do a read() on the input stream (it will
     * however cause the headers to be read and parsed if not already done).
     *
     * @return true if the response has an entity, false otherwise
     * @since V0.3-1
     */
    public synchronized boolean hasEntity() throws IOException {
        if (!got_headers)
            getHeaders(true);

        return (cd_type != CD_0);
    }

    /**
     * Should the request be retried by the application? This can be used by
     * modules to signal to the application that it should retry the request.
     * It's used when the request used an <var>HttpOutputStream</var> and the
     * module is therefore not able to retry the request itself. This flag is
     * <var>false</var> by default.
     * <p/>
     * If a module sets this flag then it must also reset() the the
     * <var>HttpOutputStream</var> so it may be reused by the application. It
     * should then also use this <var>HttpOutputStream</var> to recognize the
     * retried request in the requestHandler().
     *
     * @param flag
     *         indicates whether the application should retry the request.
     */
    public void setRetryRequest(boolean flag) {
        retry = flag;
    }

    /** @return true if the request should be retried. */
    public boolean retryRequest() {
        return retry;
    }

    // Helper Methods

    /**
     * Gets and parses the headers. Sets up Data if no data will be received.
     *
     * @param skip_cont
     *         if true skips over '100 Continue' status codes.
     * @throws IOException
     *         If any exception occurs while reading the headers.
     */
    private synchronized void getHeaders(boolean skip_cont) throws IOException {
        if (got_headers)
            return;
        if (exception != null) {
            exception.fillInStackTrace();
            throw exception;
        }

        reading_headers = true;
        try {
            do {
                Headers.clear(); // clear any headers from 100 Continue
                String headers = readResponseHeaders(inp_stream);
                parseResponseHeaders(headers);
            }
            while ((StatusCode == 100 && skip_cont) || // Continue
                   (StatusCode > 101 && StatusCode < 200)); // Unknown
        } catch (IOException ioe) {
            if (!(ioe instanceof InterruptedIOException))
                exception = ioe;
            if (ioe instanceof ProtocolException) // thrown internally
            {
                cd_type = CD_CLOSE;
                if (stream_handler != null)
                    stream_handler.markForClose(this);
            }
            throw ioe;
        } finally {
            reading_headers = false;
        }
        if (StatusCode == 100)
            return;

        // parse the Content-Length header

        int cont_len = -1;
        String cl_hdr = (String)Headers.get("Content-Length");
        if (cl_hdr != null) {
            try {
                cont_len = Integer.parseInt(cl_hdr);
                if (cont_len < 0)
                    throw new NumberFormatException();
            } catch (NumberFormatException nfe) {
                throw new ProtocolException("Invalid Content-length header" + " received: " + cl_hdr);
            }
        }

        // parse the Transfer-Encoding header

        boolean te_chunked = false, te_is_identity = true, ct_mpbr = false;
        Vector te_hdr = null;
        try {
            te_hdr = Util.parseHeader((String)Headers.get("Transfer-Encoding"));
        } catch (ParseException pe) {
        }
        if (te_hdr != null) {
            te_chunked = ((HttpHeaderElement)te_hdr.lastElement()).getName().equalsIgnoreCase("chunked");
            for (int idx = 0; idx < te_hdr.size(); idx++)
                if (((HttpHeaderElement)te_hdr.elementAt(idx)).getName().equalsIgnoreCase("identity"))
                    te_hdr.removeElementAt(idx--);
                else
                    te_is_identity = false;
        }

        // parse Content-Type header

        try {
            String hdr;
            if ((hdr = (String)Headers.get("Content-Type")) != null) {
                Vector phdr = Util.parseHeader(hdr);
                ct_mpbr =
                        phdr.contains(new HttpHeaderElement("multipart/byteranges"))
                        || phdr.contains(new HttpHeaderElement("multipart/x-byteranges"));
            }
        } catch (ParseException pe) {
        }

        // now determine content-delimiter

        if (StatusCode < 200 || StatusCode == 204 || StatusCode == 205 || StatusCode == 304) {
            cd_type = CD_0;
        } else if (te_chunked) {
            cd_type = CD_CHUNKED;

            te_hdr.removeElementAt(te_hdr.size() - 1);
            if (te_hdr.size() > 0)
                setHeader("Transfer-Encoding", Util.assembleHeader(te_hdr));
            else
                deleteHeader("Transfer-Encoding");
        } else if (cont_len != -1 && te_is_identity)
            cd_type = CD_CONTLEN;
        else if (ct_mpbr && te_is_identity)
            cd_type = CD_MP_BR;
        else if (!method.equals("HEAD")) {
            cd_type = CD_CLOSE;
            if (stream_handler != null)
                stream_handler.markForClose(this);

            if (Version.equals("HTTP/0.9")) {
                inp_stream = new SequenceInputStream(new ByteArrayInputStream(Data), inp_stream);
                Data = null;
            }
        }

        if (cd_type == CD_CONTLEN)
            ContentLength = cont_len;
        else
            deleteHeader("Content-Length"); // Content-Length is not valid in this
        // case

      /*
       * We treat HEAD specially down here because the above code needs to know
       * whether to remove the Content-length header or not.
       */
        if (method.equals("HEAD"))
            cd_type = CD_0;

        if (cd_type == CD_0) {
            ContentLength = 0;
            Data = new byte[0];
            inp_stream.close(); // we will not receive any more data
        }

        if (log.isDebugEnabled()) {
            log.debug("Response entity delimiter: "
                      + (cd_type == CD_0 ? "No Entity" : cd_type == CD_CLOSE ? "Close" : cd_type == CD_CONTLEN ? "Content-Length"
                                                                                                               : cd_type == CD_CHUNKED
                                                                                                                 ? "Chunked"
                                                                                                                 : cd_type == CD_MP_BR
                                                                                                                   ? "Multipart" : "???") +
                      " ("
                      + inp_stream.hashCode() + ")");
        }

        // remove erroneous connection tokens

        if (connection.ServerProtocolVersion >= HTTP_1_1)
            deleteHeader("Proxy-Connection");
        else
        // HTTP/1.0
        {
            if (connection.getProxyHost() != null)
                deleteHeader("Connection");
            else
                deleteHeader("Proxy-Connection");

            Vector pco;
            try {
                pco = Util.parseHeader((String)Headers.get("Connection"));
            } catch (ParseException pe) {
                pco = null;
            }

            if (pco != null) {
                for (int idx = 0; idx < pco.size(); idx++) {
                    String name = ((HttpHeaderElement)pco.elementAt(idx)).getName();
                    if (!name.equalsIgnoreCase("keep-alive")) {
                        pco.removeElementAt(idx);
                        deleteHeader(name);
                        idx--;
                    }
                }

                if (pco.size() > 0)
                    setHeader("Connection", Util.assembleHeader(pco));
                else
                    deleteHeader("Connection");
            }

            try {
                pco = Util.parseHeader((String)Headers.get("Proxy-Connection"));
            } catch (ParseException pe) {
                pco = null;
            }

            if (pco != null) {
                for (int idx = 0; idx < pco.size(); idx++) {
                    String name = ((HttpHeaderElement)pco.elementAt(idx)).getName();
                    if (!name.equalsIgnoreCase("keep-alive")) {
                        pco.removeElementAt(idx);
                        deleteHeader(name);
                        idx--;
                    }
                }

                if (pco.size() > 0)
                    setHeader("Proxy-Connection", Util.assembleHeader(pco));
                else
                    deleteHeader("Proxy-Connection");
            }
        }

        // this must be set before we invoke handleFirstRequest()
        got_headers = true;

        // special handling if this is the first response received
        if (isFirstResponse) {
            if (!connection.handleFirstRequest(req, this)) {
                // got a buggy server - need to redo the request
                Response resp;
                try {
                    resp = connection.sendRequest(req, timeout);
                } catch (ModuleException me) {
                    throw new IOException(me.toString());
                }
                resp.getVersion();

                this.StatusCode = resp.StatusCode;
                this.ReasonLine = resp.ReasonLine;
                this.Version = resp.Version;
                this.EffectiveURI = resp.EffectiveURI;
                this.ContentLength = resp.ContentLength;
                this.Headers = resp.Headers;
                this.inp_stream = resp.inp_stream;
                this.Data = resp.Data;

                req = null;
            }
        }
    }

    /*
     * these are external to readResponseHeaders() because we need to be able to
     * restart after an InterruptedIOException
     */
    private byte[] buf = new byte[7];

    private int buf_pos = 0;

    private StringBuffer hdrs = new StringBuffer(400);

    private boolean reading_lines = false;

    private boolean bol = true;

    private boolean got_cr = false;

    /**
     * Reads the response headers received, folding continued lines.
     * <p/>
     * Some of the code is a bit convoluted because we have to be able restart
     * after an InterruptedIOException.
     *
     * @return a (newline separated) list of headers
     * @throws IOException
     *         if any read on the input stream fails
     * @inp the input stream from which to read the response
     */
    private String readResponseHeaders(InputStream inp) throws IOException {
        if (log.isDebugEnabled()) {
            if (buf_pos == 0)
                log.debug("Reading Response headers " + inp_stream.hashCode());
            else
                log.debug("Resuming reading Response headers " + inp_stream.hashCode());
        }

        // read 7 bytes to see type of response
        if (!reading_lines) {
            try {
                // Skip any leading white space to accomodate buggy responses
                if (buf_pos == 0) {
                    int c;
                    do {
                        if ((c = inp.read()) == -1)
                            throw new EOFException("Encountered premature EOF " + "while reading Version");
                    }
                    while (Character.isWhitespace((char)c));
                    buf[0] = (byte)c;
                    buf_pos = 1;
                }

                // Now read first seven bytes (the version string)
                while (buf_pos < buf.length) {
                    int got = inp.read(buf, buf_pos, buf.length - buf_pos);
                    if (got == -1)
                        throw new EOFException("Encountered premature EOF " + "while reading Version");
                    buf_pos += got;
                }
            } catch (EOFException eof) {
                log.error("Response:  (" + inp_stream.hashCode() + ")", eof);

                throw eof;
            }
            for (int idx = 0; idx < buf.length; idx++)
                hdrs.append((char)buf[idx]);

            reading_lines = true;
        }

        if (hdrs.toString().startsWith("HTTP/") || // It's x.x
            hdrs.toString().startsWith("HTTP ")) // NCSA bug
            readLines(inp);

        // reset variables for next round
        buf_pos = 0;
        reading_lines = false;
        bol = true;
        got_cr = false;

        String tmp = hdrs.toString();
        hdrs.setLength(0);
        return tmp;
    }

    boolean trailers_read = false;

    /**
     * This is called by the StreamDemultiplexor to read all the trailers of a
     * chunked encoded entity.
     *
     * @param inp
     *         the raw input stream to read from
     * @throws IOException
     *         if any IOException is thrown by the stream
     */
    void readTrailers(InputStream inp) throws IOException {
        try {
            readLines(inp);
            trailers_read = true;
        } catch (IOException ioe) {
            if (!(ioe instanceof InterruptedIOException))
                exception = ioe;
            throw ioe;
        }
    }

    /**
     * This reads a set of lines up to and including the first empty line. A line
     * is terminated by either a <CR><LF> or <LF>. The lines are stored in the
     * <var>hdrs</var> buffers. Continued lines are merged and stored as one
     * line.
     * <p/>
     * This method is restartable after an InterruptedIOException.
     *
     * @param inp
     *         the input stream to read from
     * @throws IOException
     *         if any IOException is thrown by the stream
     */
    private void readLines(InputStream inp) throws IOException {
      /*
       * This loop is a merge of readLine() from DataInputStream and the necessary
       * header logic to merge continued lines and terminate after an empty line.
       * The reason this is explicit is because of the need to handle
       * InterruptedIOExceptions.
       */
        loop:
        while (true) {
            int b = inp.read();
            switch (b) {
                case -1:
                    throw new EOFException("Encountered premature EOF while reading headers:\n" + hdrs);
                case '\r':
                    got_cr = true;
                    break;
                case '\n':
                    if (bol)
                        break loop; // all headers read
                    hdrs.append('\n');
                    bol = true;
                    got_cr = false;
                    break;
                case ' ':
                case '\t':
                    if (bol) // a continued line
                    {
                        // replace previous \n with SP
                        hdrs.setCharAt(hdrs.length() - 1, ' ');
                        bol = false;
                        break;
                    }
                default:
                    if (got_cr) {
                        hdrs.append('\r');
                        got_cr = false;
                    }
                    hdrs.append((char)(b & 0xFF));
                    bol = false;
                    break;
            }
        }
    }

    /**
     * Parses the headers received into a new Response structure.
     *
     * @param headers
     *         a (newline separated) list of headers
     * @throws ProtocolException
     *         if any part of the headers do not conform
     */
    private void parseResponseHeaders(String headers) throws ProtocolException {
        String sts_line = null;
        StringTokenizer lines = new StringTokenizer(headers, "\r\n"), elem;

        if (log.isDebugEnabled())
            log.debug("Parsing Response headers from Request " + "\"" + method + " " + resource + "\":  ("
                      + inp_stream.hashCode() + ")\n\n" + headers);

        // Detect and handle HTTP/0.9 responses

        if (!headers.regionMatches(true, 0, "HTTP/", 0, 5) && !headers.regionMatches(true, 0, "HTTP ", 0, 5)) // NCSA bug
        {
            Version = "HTTP/0.9";
            StatusCode = 200;
            ReasonLine = "OK";

            try {
                Data = headers.getBytes("8859_1");
            } catch (UnsupportedEncodingException uee) {
                throw new Error(uee.toString());
            }

            return;
        }

        // get the status line

        try {
            sts_line = lines.nextToken();
            elem = new StringTokenizer(sts_line, " \t");

            Version = elem.nextToken();
            StatusCode = Integer.valueOf(elem.nextToken()).intValue();

            if (Version.equalsIgnoreCase("HTTP")) // NCSA bug
                Version = "HTTP/1.0";
        } catch (NoSuchElementException e) {
            throw new ProtocolException("Invalid HTTP status line received: " + sts_line);
        }
        try {
            ReasonLine = elem.nextToken("").trim();
        } catch (NoSuchElementException e) {
            ReasonLine = "";
        }

      /*
       * If the status code shows an error and we're sending (or have sent) an
       * entity and it's length is delimited by a Content-length header, then we
       * must close the the connection (if indeed it hasn't already been done) -
       * RFC-2616, Section 8.2.2 .
       */
        if (StatusCode >= 300 && sent_entity) {
            if (stream_handler != null)
                stream_handler.markForClose(this);
        }

        // get the rest of the headers

        parseHeaderFields(lines, Headers);

      /*
       * make sure the connection isn't closed prematurely if we have trailer
       * fields
       */
        if (Headers.get("Trailer") != null && resp_inp_stream != null)
            resp_inp_stream.dontTruncate();

        // Mark the end of the connection if it's not to be kept alive

        int vers;
        if (Version.equalsIgnoreCase("HTTP/0.9") || Version.equalsIgnoreCase("HTTP/1.0"))
            vers = 0;
        else
            vers = 1;

        try {
            String con = (String)Headers.get("Connection"), pcon = (String)Headers.get("Proxy-Connection");

            // parse connection header
            if ((vers == 1 && con != null && Util.hasToken(con, "close"))
                || (vers == 0 && !((!used_proxy && con != null && Util.hasToken(con, "keep-alive")) || (used_proxy
                                                                                                        && pcon != null &&
                                                                                                        Util.hasToken(pcon,
                                                                                                                      "keep-alive")))))
                if (stream_handler != null)
                    stream_handler.markForClose(this);
        } catch (ParseException pe) {
        }
    }

    /**
     * If the trailers have not been read it calls <code>getData()</code> to
     * first force all data and trailers to be read. Then the trailers parsed
     * into the <var>Trailers</var> hashtable.
     *
     * @throws IOException
     *         if any exception occured during reading of the
     *         response
     */
    private synchronized void getTrailers() throws IOException {
        if (got_trailers)
            return;
        if (exception != null) {
            exception.fillInStackTrace();
            throw exception;
        }

        if (log.isDebugEnabled())
            log.debug("Reading Response trailers " + inp_stream.hashCode());

        try {
            if (!trailers_read) {
                if (resp_inp_stream != null)
                    resp_inp_stream.readAll(timeout);
            }

            if (trailers_read) {
                if (log.isDebugEnabled())
                    log.debug("Parsing Response trailers from " + "Request \"" + method + " " + resource + "\":  ("
                              + inp_stream.hashCode() + ")\n\n" + hdrs);

                parseHeaderFields(new StringTokenizer(hdrs.toString(), "\r\n"), Trailers);
            }
        } finally {
            got_trailers = true;
        }
    }

    /**
     * Parses the given lines as header fields of the form "<name>: <value>" into
     * the given list.
     *
     * @param lines
     *         the header or trailer lines, one header field per line
     * @param list
     *         the Hashtable to store the parsed fields in
     * @throws ProtocolException
     *         if any part of the headers do not conform
     */
    private void parseHeaderFields(StringTokenizer lines, CIHashtable list) throws ProtocolException {
        while (lines.hasMoreTokens()) {
            String hdr = lines.nextToken();
            int sep = hdr.indexOf(':');

         /*
          * Once again we have to deal with broken servers and try to wing it here.
          * If no ':' is found, try using the first space:
          */
            if (sep == -1)
                sep = hdr.indexOf(' ');
            if (sep == -1) {
                throw new ProtocolException("Invalid HTTP header received: " + hdr);
            }

            String hdr_name = hdr.substring(0, sep).trim();
            String hdr_value = hdr.substring(sep + 1).trim();

            // Can header have multiple values?
            if (!singleValueHeaders.containsKey(hdr_name.toLowerCase())) {
                String old_value = (String)list.get(hdr_name);
                if (old_value == null)
                    list.put(hdr_name, hdr_value);
                else
                    list.put(hdr_name, old_value + ", " + hdr_value);
            } else
                // No multiple values--just replace/put latest header value
                list.put(hdr_name, hdr_value);
        }
    }

    /**
     * Reads the response data received. Does not return until either
     * Content-Length bytes have been read or EOF is reached.
     *
     * @throws IOException
     *         if any read on the input stream fails
     * @inp the input stream from which to read the data
     */
    private void readResponseData(InputStream inp) throws IOException {
        if (ContentLength == 0)
            return;

        if (Data == null)
            Data = new byte[0];

        // read response data

        int off = Data.length;

        try {
            // check Content-length header in case CE-Module removed it
            if (getHeader("Content-Length") != null) {
                int rcvd = 0;
                Data = new byte[ContentLength];

                do {
                    off += rcvd;
                    rcvd = inp.read(Data, off, ContentLength - off);
                }
                while (rcvd != -1 && off + rcvd < ContentLength);

            /*
             * Don't do this! If we do, then getData() won't work after a
             * getInputStream() because we'll never get all the expected data.
             * Instead, let the underlying RespInputStream throw the EOF. if (rcvd
             * == -1) // premature EOF { throw new EOFException("Encountered
             * premature EOF while " + "reading headers: received " + off + " bytes
             * instead of the expected " + ContentLength + " bytes"); }
             */
            } else {
                int inc = 1000, rcvd = 0;

                do {
                    off += rcvd;
                    Data = Util.resizeArray(Data, off + inc);
                }
                while ((rcvd = inp.read(Data, off, inc)) != -1);

                Data = Util.resizeArray(Data, off);
            }
        } catch (IOException ioe) {
            Data = Util.resizeArray(Data, off);
            throw ioe;
        } finally {
            try {
                inp.close();
            } catch (IOException ioe) {
            }
        }
    }

    Request req = null;

    boolean isFirstResponse = false;

    /**
     * This marks this response as belonging to the first request made over an
     * HTTPConnection. The <var>con</var> and <var>req</var> parameters are
     * needed in case we have to do a resend of the request - this is to handle
     * buggy servers which barf upon receiving a request marked as HTTP/1.1 .
     *
     * @param con
     *         The HTTPConnection used
     * @param req
     *         The Request sent
     */
    void markAsFirstResponse(Request req) {
        this.req = req;
        isFirstResponse = true;
    }

    /** @return a clone of this request object */
    public Object clone() {
        Response cl;
        try {
            cl = (Response)super.clone();
        } catch (CloneNotSupportedException cnse) {
            throw new InternalError(cnse.toString()); /* shouldn't happen */
        }

        cl.Headers = (CIHashtable)Headers.clone();
        cl.Trailers = (CIHashtable)Trailers.clone();

        return cl;
    }
}
